// Copyright 2003-2005 Arthur van Hoff, Rick Blair
// Licensed under Apache License version 2.0
// Original license LGPL
package javax.jmdns.impl;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Iterator;

import android.util.Log;

/** DNS record
 * @version %I%, %G%
 * @author Arthur van Hoff, Rick Blair, Werner Randelshofer, Pierre Frisch */
public abstract class DNSRecord extends DNSEntry {
	public final static String TAG = DNSRecord.class.toString();
	private int ttl;
	private long created;
	/** This source is mainly for debugging purposes, should be the address that
	 * sent this record. */
	private InetAddress source;

	/** Create a DNSRecord with a name, type, clazz, and ttl. */
	DNSRecord(String name, int type, int clazz, int ttl) {
		super(name, type, clazz);
		this.ttl = ttl;
		this.created = System.currentTimeMillis();
	}

	/** True if this record is the same as some other record. */
	public boolean equals(Object other) {
		return (other instanceof DNSRecord) && sameAs((DNSRecord) other);
	}

	/** True if this record is the same as some other record. */
	boolean sameAs(DNSRecord other) {
		return super.equals(other) && sameValue((DNSRecord) other);
	}

	/** True if this record has the same value as some other record. */
	abstract boolean sameValue(DNSRecord other);

	/** True if this record has the same type as some other record. */
	boolean sameType(DNSRecord other) {
		return type == other.type;
	}

	/** Handles a query represented by this record.
	 * @return Returns true if a conflict with one of the services registered
	 * with JmDNS or with the hostname occured. */
	abstract boolean handleQuery(JmDNSImpl dns, long expirationTime);

	/** Handles a responserepresented by this record.
	 * @return Returns true if a conflict with one of the services registered
	 * with JmDNS or with the hostname occured. */
	abstract boolean handleResponse(JmDNSImpl dns);

	/** Adds this as an answer to the provided outgoing datagram. */
	abstract DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in,
			InetAddress addr, int port, DNSOutgoing out) throws IOException;

	/** True if this record is suppressed by the answers in a message. */
	boolean suppressedBy(DNSIncoming msg) {
		try {
			for (int i = msg.numAnswers; i-- > 0;) {
				if (suppressedBy((DNSRecord) msg.answers.get(i))) {
					return true;
				}
			}
			return false;
		} catch (ArrayIndexOutOfBoundsException e) {
			Log.d(TAG, "suppressedBy() message " + msg + " exception ");
			// msg.print(true);
			return false;
		}
	}

	/** True if this record would be supressed by an answer. This is the case if
	 * this record would not have a significantly longer TTL. */
	boolean suppressedBy(DNSRecord other) {
		if (sameAs(other) && (other.ttl > ttl / 2)) {
			return true;
		}
		return false;
	}

	/** Get the expiration time of this record. */
	long getExpirationTime(int percent) {
		return created + (percent * ttl * 10L);
	}

	/** Get the remaining TTL for this record. */
	int getRemainingTTL(long now) {
		return (int) Math.max(0, (getExpirationTime(100) - now) / 1000);
	}

	/** Check if the record is expired. */
	public boolean isExpired(long now) {
		return getExpirationTime(100) <= now;
	}

	/** Check if the record is stale, ie it has outlived more than half of its
	 * TTL. */
	boolean isStale(long now) {
		return getExpirationTime(50) <= now;
	}

	/** Reset the TTL of a record. This avoids having to update the entire record
	 * in the cache. */
	void resetTTL(DNSRecord other) {
		created = other.created;
		ttl = other.ttl;
	}

	/** Write this record into an outgoing message. */
	abstract void write(DNSOutgoing out) throws IOException;

	/** Address record. */
	public static class Address extends DNSRecord {
		InetAddress addr;

		Address(String name, int type, int clazz, int ttl, InetAddress addr) {
			super(name, type, clazz, ttl);
			this.addr = addr;
		}

		Address(String name, int type, int clazz, int ttl, byte[] rawAddress) {
			super(name, type, clazz, ttl);
			try {
				this.addr = InetAddress.getByAddress(rawAddress);
			} catch (UnknownHostException exception) {
				Log.d(TAG, "Address() exception ", exception);
			}
		}

		void write(DNSOutgoing out) throws IOException {
			if (addr != null) {
				byte[] buffer = addr.getAddress();
				if (DNSConstants.TYPE_A == type) {
					// If we have a type A records we should answer with a IPv4
					// address
					if (addr instanceof Inet4Address) {
						// All is good
					} else {
						// Get the last four bytes
						byte[] tempbuffer = buffer;
						buffer = new byte[4];
						System.arraycopy(tempbuffer, 12, buffer, 0, 4);
					}
				} else {
					// If we have a type AAAA records we should answer with a
					// IPv6
					// address
					if (addr instanceof Inet4Address) {
						byte[] tempbuffer = buffer;
						buffer = new byte[16];
						for (int i = 0; i < 16; i++) {
							if (i < 11) {
								buffer[i] = tempbuffer[i - 12];
							} else {
								buffer[i] = 0;
							}
						}
					}
				}
				int length = buffer.length;
				out.writeBytes(buffer, 0, length);
			}
		}

		boolean same(DNSRecord other) {
			return ((sameName(other)) && ((sameValue(other))));
		}

		boolean sameName(DNSRecord other) {
			return name.equalsIgnoreCase(((Address) other).name);
		}

		boolean sameValue(DNSRecord other) {
			return addr.equals(((Address) other).getAddress());
		}

		public InetAddress getAddress() {
			return addr;
		}

		/** Creates a byte array representation of this record. This is needed
		 * for tie-break tests according to
		 * draft-cheshire-dnsext-multicastdns-04.txt chapter 9.2. */
		private byte[] toByteArray() {
			try {
				ByteArrayOutputStream bout = new ByteArrayOutputStream();
				DataOutputStream dout = new DataOutputStream(bout);
				dout.write(name.getBytes("UTF8"));
				dout.writeShort(type);
				dout.writeShort(clazz);
				// dout.writeInt(len);
				byte[] buffer = addr.getAddress();
				for (int i = 0; i < buffer.length; i++) {
					dout.writeByte(buffer[i]);
				}
				dout.close();
				return bout.toByteArray();
			} catch (IOException e) {
				throw new InternalError();
			}
		}

		/** Does a lexicographic comparison of the byte array representation of
		 * this record and that record. This is needed for tie-break tests
		 * according to draft-cheshire-dnsext-multicastdns-04.txt chapter 9.2. */
		private int lexCompare(DNSRecord.Address that) {
			byte[] thisBytes = this.toByteArray();
			byte[] thatBytes = that.toByteArray();
			for (int i = 0, n = Math.min(thisBytes.length, thatBytes.length); i < n; i++) {
				if (thisBytes[i] > thatBytes[i]) {
					return 1;
				} else {
					if (thisBytes[i] < thatBytes[i]) {
						return -1;
					}
				}
			}
			return thisBytes.length - thatBytes.length;
		}

		/** Does the necessary actions, when this as a query. */
		@SuppressWarnings("rawtypes")
		boolean handleQuery(JmDNSImpl dns, long expirationTime) {
			DNSRecord.Address dnsAddress = dns.getLocalHost()
					.getDNSAddressRecord(this);
			if (dnsAddress != null) {
				if (dnsAddress.sameType(this) && dnsAddress.sameName(this)
						&& (!dnsAddress.sameValue(this))) {
					Log.d(TAG,
							"handleQuery() Conflicting probe detected. dns state "
									+ dns.getState() + " lex compare "
									+ lexCompare(dnsAddress));
					// Tie-breaker test
					if (dns.getState().isProbing()
							&& lexCompare(dnsAddress) >= 0) {
						// We lost the tie-break. We have to choose a different
						// name.
						dns.getLocalHost().incrementHostName();
						dns.getCache().clear();
						for (Iterator i = dns.getServices().values().iterator(); i
								.hasNext();) {
							ServiceInfoImpl info = (ServiceInfoImpl) i.next();
							info.revertState();
						}
					}
					dns.revertState();
					return true;
				}
			}
			return false;
		}

		/** Does the necessary actions, when this as a response. */
		@SuppressWarnings("rawtypes")
		boolean handleResponse(JmDNSImpl dns) {
			DNSRecord.Address dnsAddress = dns.getLocalHost()
					.getDNSAddressRecord(this);
			if (dnsAddress != null) {
				if (dnsAddress.sameType(this) && dnsAddress.sameName(this)
						&& (!dnsAddress.sameValue(this))) {
					Log.d(TAG, "handleResponse() Denial detected");
					if (dns.getState().isProbing()) {
						dns.getLocalHost().incrementHostName();
						dns.getCache().clear();
						for (Iterator i = dns.getServices().values().iterator(); i
								.hasNext();) {
							ServiceInfoImpl info = (ServiceInfoImpl) i.next();
							info.revertState();
						}
					}
					dns.revertState();
					return true;
				}
			}
			return false;
		}

		DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr,
				int port, DNSOutgoing out) throws IOException {
			return out;
		}

		public String toString() {
			return toString(" address '"
					+ (addr != null ? addr.getHostAddress() : "null") + "'");
		}
	}

	/** Pointer record. */
	public static class Pointer extends DNSRecord {
		String alias;

		public Pointer(String name, int type, int clazz, int ttl, String alias) {
			super(name, type, clazz, ttl);
			this.alias = alias;
		}

		void write(DNSOutgoing out) throws IOException {
			out.writeName(alias);
		}

		boolean sameValue(DNSRecord other) {
			return alias.equals(((Pointer) other).alias);
		}

		boolean handleQuery(JmDNSImpl dns, long expirationTime) {
			// Nothing to do (?)
			// I think there is no possibility for conflicts for this record
			// type?
			return false;
		}

		boolean handleResponse(JmDNSImpl dns) {
			// Nothing to do (?)
			// I think there is no possibility for conflicts for this record
			// type?
			return false;
		}

		String getAlias() {
			return alias;
		}

		DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr,
				int port, DNSOutgoing out) throws IOException {
			return out;
		}

		public String toString() {
			return toString(alias);
		}
	}

	public static class Text extends DNSRecord {
		public byte text[];

		public Text(String name, int type, int clazz, int ttl, byte text[]) {
			super(name, type, clazz, ttl);
			this.text = text;
		}

		void write(DNSOutgoing out) throws IOException {
			out.writeBytes(text, 0, text.length);
		}

		boolean sameValue(DNSRecord other) {
			Text txt = (Text) other;
			if (txt.text.length != text.length) {
				return false;
			}
			for (int i = text.length; i-- > 0;) {
				if (txt.text[i] != text[i]) {
					return false;
				}
			}
			return true;
		}

		boolean handleQuery(JmDNSImpl dns, long expirationTime) {
			// Nothing to do (?)
			// I think there is no possibility for conflicts for this record
			// type?
			return false;
		}

		boolean handleResponse(JmDNSImpl dns) {
			// Nothing to do (?)
			// Shouldn't we care if we get a conflict at this level?
			/*
			 * ServiceInfo info = (ServiceInfo)
			 * dns.services.get(name.toLowerCase()); if (info != null) { if (!
			 * Arrays.equals(text,info.text)) { info.revertState(); return true;
			 * } }
			 */
			return false;
		}

		DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr,
				int port, DNSOutgoing out) throws IOException {
			return out;
		}

		public String toString() {
			return toString((text.length > 10) ? new String(text, 0, 7) + "..."
					: new String(text));
		}
	}

	/** Service record. */
	public static class Service extends DNSRecord {
		int priority;
		int weight;
		public int port;
		public String server;

		public Service(String name, int type, int clazz, int ttl, int priority,
				int weight, int port, String server) {
			super(name, type, clazz, ttl);
			this.priority = priority;
			this.weight = weight;
			this.port = port;
			this.server = server;
		}

		void write(DNSOutgoing out) throws IOException {
			out.writeShort(priority);
			out.writeShort(weight);
			out.writeShort(port);
			if (DNSIncoming.USE_DOMAIN_NAME_FORMAT_FOR_SRV_TARGET) {
				out.writeName(server, false);
			} else {
				out.writeUTF(server, 0, server.length());
				// add a zero byte to the end just to be safe, this is the
				// strange
				// form
				// used by the BonjourConformanceTest
				out.writeByte(0);
			}
		}

		private byte[] toByteArray() {
			try {
				ByteArrayOutputStream bout = new ByteArrayOutputStream();
				DataOutputStream dout = new DataOutputStream(bout);
				dout.write(name.getBytes("UTF8"));
				dout.writeShort(type);
				dout.writeShort(clazz);
				// dout.writeInt(len);
				dout.writeShort(priority);
				dout.writeShort(weight);
				dout.writeShort(port);
				dout.write(server.getBytes("UTF8"));
				dout.close();
				return bout.toByteArray();
			} catch (IOException e) {
				throw new InternalError();
			}
		}

		private int lexCompare(DNSRecord.Service that) {
			byte[] thisBytes = this.toByteArray();
			byte[] thatBytes = that.toByteArray();
			for (int i = 0, n = Math.min(thisBytes.length, thatBytes.length); i < n; i++) {
				if (thisBytes[i] > thatBytes[i]) {
					return 1;
				} else {
					if (thisBytes[i] < thatBytes[i]) {
						return -1;
					}
				}
			}
			return thisBytes.length - thatBytes.length;
		}

		boolean sameValue(DNSRecord other) {
			Service s = (Service) other;
			return (priority == s.priority) && (weight == s.weight)
					&& (port == s.port) && server.equals(s.server);
		}

		@SuppressWarnings("unchecked")
		boolean handleQuery(JmDNSImpl dns, long expirationTime) {
			ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(
					name.toLowerCase());
			if (info != null
					&& (port != info.port || !server.equalsIgnoreCase(dns
							.getLocalHost().getName()))) {
				Log.d(TAG, "handleQuery() Conflicting probe detected from: "
						+ getRecordSource());
				DNSRecord.Service localService = new DNSRecord.Service(
						info.getQualifiedName(), DNSConstants.TYPE_SRV,
						DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE,
						DNSConstants.DNS_TTL, info.priority, info.weight,
						info.port, dns.getLocalHost().getName());
				// This block is useful for debugging race conditions when jmdns
				// is
				// respoding to
				// itself.
				try {
					if (dns.getInterface().equals(getRecordSource())) {
						Log.d(TAG, "Got conflicting probe from ourselves\n"
								+ "incoming: " + this.toString() + "\n"
								+ "local   : " + localService.toString());
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
				int comparison = lexCompare(localService);
				if (comparison == 0) {
					// the 2 records are identical this probably means we are
					// seeing
					// our own record.
					// With mutliple interfaces on a single computer it is
					// possible
					// to see our
					// own records come in on different interfaces than the ones
					// they
					// were sent on.
					// see section "10. Conflict Resolution" of mdns draft spec.
					Log.d(TAG,
							"handleQuery() Ignoring a identical service query");
					return false;
				}
				// Tie breaker test
				if (info.getState().isProbing() && comparison > 0) {
					// We lost the tie break
					String oldName = info.getQualifiedName().toLowerCase();
					info.setName(dns.incrementName(info.getName()));
					dns.getServices().remove(oldName);
					dns.getServices().put(
							info.getQualifiedName().toLowerCase(), info);
					Log.d(TAG,
							"handleQuery() Lost tie break: new unique name chosen:"
									+ info.getName());
					// We revert the state to start probing again with the new
					// name
					info.revertState();
				} else {
					// We won the tie break, so this conflicting probe should be
					// ignored
					// See paragraph 3 of section 9.2 in mdns draft spec
					return false;
				}
				return true;
			}
			return false;
		}

		@SuppressWarnings("unchecked")
		boolean handleResponse(JmDNSImpl dns) {
			ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(
					name.toLowerCase());
			if (info != null
					&& (port != info.port || !server.equalsIgnoreCase(dns
							.getLocalHost().getName()))) {
				Log.d(TAG, "handleResponse() Denial detected");
				if (info.getState().isProbing()) {
					String oldName = info.getQualifiedName().toLowerCase();
					info.setName(dns.incrementName(info.getName()));
					dns.getServices().remove(oldName);
					dns.getServices().put(
							info.getQualifiedName().toLowerCase(), info);
					Log.d(TAG,
							"handleResponse() New unique name chose:"
									+ info.getName());
				}
				info.revertState();
				return true;
			}
			return false;
		}

		DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr,
				int port, DNSOutgoing out) throws IOException {
			ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(
					name.toLowerCase());
			if (info != null) {
				if (this.port == info.port != server.equals(dns.getLocalHost()
						.getName())) {
					return dns.addAnswer(in, addr, port, out,
							new DNSRecord.Service(info.getQualifiedName(),
									DNSConstants.TYPE_SRV,
									DNSConstants.CLASS_IN
											| DNSConstants.CLASS_UNIQUE,
									DNSConstants.DNS_TTL, info.priority,
									info.weight, info.port, dns.getLocalHost()
											.getName()));
				}
			}
			return out;
		}

		public String toString() {
			return toString(server + ":" + port);
		}
	}

	public void setRecordSource(InetAddress source) {
		this.source = source;
	}

	public InetAddress getRecordSource() {
		return source;
	}

	public String toString(String other) {
		return toString("record",
				ttl + "/" + getRemainingTTL(System.currentTimeMillis()) + ","
						+ other);
	}

	public void setTtl(int ttl) {
		this.ttl = ttl;
	}

	public int getTtl() {
		return ttl;
	}
}